/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package czStvDpds.graphics3D;

import czStvDpds.graphics3D.geometry.Object3D;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collections;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.vector.Matrix4f;

/**
 * Objekt reprezentující vykreslovací část grafiky.
 *
 * @author divad
 */
public class Scene {

    private ArrayList<Object3D> objects;
    private int transparentObjectsOffset = -1;
    protected Matrix4f camPos;
    protected FloatBuffer matrixStorer;
    protected Matrix4f viewFrustum;
    private int x, y, width, height;
    protected float sideRatio, near, far, fov, centreX, centreY, relativeHeight, relativeWidth;
    private boolean invertWidth, invertHeight;
    protected GLSettings glSettings;
    private boolean userDraw = false;
    protected SceneSettings sceneSettings;
    private ArrayList<Scene> firstDrawScenes, thirdDrawScenes;
    private GraphicsObjectComparator objectComparator;
    private boolean enabled = true;

    /**
     * @param x - Xová pozice levého dolního rohu viewportu (pozice 0,0 je levý
     * dolní roh okna)
     * @param y -Yová pozice levého dolního rohu viewportu (pozice 0,0 je levý
     * dolní roh okna)
     * @param width - šířka viewportu v pixlech
     * @param height - výška viewportu v pixlech
     * @param sideRatio - poměr stran (šířka / výška)
     * @param near - nejbližší ořezávací rovina. Musí být >0. Pixly zobrazující
     * se před touto rovniou nebudou vykresleny
     * @param far - nejvzdálenější ořezávací rovina. Pixly zobrazující se za
     * touto rovniou nebudou vykresleny
     * @param fov - vrcholový úhel pohledové pyramidy. Čím větší, tím více se
     * vzdálené objekty zmenšují. Pokud se rovná 0, zobrazuje se v pravoúhlém
     * promítání.
     * @param centreX - hodnota označující Xovou souřadnici počátku pohledové
     * matice v okně. 0 - levá strana; 1 - pravá strana; 0.5 - uprostřed
     * @param centreY - hodnota označující Yovou souřadnici počátku pohledové
     * matice v okně. 0 - dolní strana; 1 - horní strana; 0.5 - uprostřed
     * @param relativeWidth - počet vykreslovacích jednotek připadajících na
     * šířku scény v px. Má význam pouze pokud fov = 0 (ortho režim).
     * @param invertWidth - Značí jestli má být obraz osově převrácený podle osy
     * Y.
     * @param invertHeight - Značí jestli má být obraz osově převrácený podle
     * osy X.
     */
    public Scene(int x, int y, int width, int height, float sideRatio, float near, float far, float fov, float centreX, float centreY, float relativeWidth, boolean invertWidth, boolean invertHeight) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.sideRatio = sideRatio;
        this.near = near;
        this.far = far;
        this.fov = fov;
        this.centreX = centreX;
        this.centreY = centreY;
        this.relativeWidth = relativeWidth;
        this.invertHeight = invertHeight;
        this.invertWidth = invertWidth;

        camPos = new Matrix4f();
        viewFrustum = new Matrix4f();
        matrixStorer = BufferUtils.createFloatBuffer(16);

        this.objects = new ArrayList<>();
        this.firstDrawScenes = new ArrayList();
        this.thirdDrawScenes = new ArrayList();
        this.glSettings = new GLSettings();
        this.sceneSettings = new SceneSettings();
        this.objectComparator = new GraphicsObjectComparator();

        this.setRelativeDimension(fov, sideRatio, near);
    }

    private void setRelativeDimension(float fov, float sideRatio, float near) {
        if (fov == 0) {
//            this.relativeWidth = 1;
//            this.relativeHeight = 1 / sideRatio;
            this.relativeHeight = relativeWidth / sideRatio;
        } else {
            float side = (float) Math.tan(Math.toRadians(fov / 2)) * 0.5f;
            this.relativeWidth = side * 2;
            this.relativeHeight = (side / sideRatio) * 2;
        }
    }

    protected void predrawSetup(Scene owner) {
        throw new UnsupportedOperationException("Need to be overriden");
    }

    protected void userDraw() {
        throw new UnsupportedOperationException("Need to be overriden");
    }

    /**
     * vykreslí scénu. Mělo by být voláno pouze řídícím prvkem grafiky.
     */
    public void draw(Scene owner) {

//        GL11.glLoadIdentity();
//        GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_COLOR_BUFFER_BIT);
        if (this.enabled) {
            if (this.userDraw) {
                try {
                    this.userDraw();
                    this.setupGL(x, y, width, height, sideRatio, near, far, fov, centreX, centreY, this.invertWidth, this.invertHeight);
                    GL11.glLoadIdentity();
                } catch (UnsupportedOperationException ex) {
                }
            }

            for (int index = 0; index < this.firstDrawScenes.size(); index++) {
                Scene scene;
                synchronized (this.firstDrawScenes) {
                    scene = this.firstDrawScenes.get(index);
                }
                scene.draw(this);
            }

            if (this.sceneSettings.isClearColorOnRepaint()) {
                GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
            }
            if (this.sceneSettings.isClearDepthOnRepaint()) {
                GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT);
            }

            this.setupGL(x, y, width, height, sideRatio, near, far, fov, centreX, centreY, this.invertWidth, this.invertHeight);

            try {
                this.predrawSetup(owner);
            } catch (UnsupportedOperationException ex) {
            }

            this.sortObjects();

            camPos.store(matrixStorer);
            matrixStorer.rewind();
            GL11.glLoadMatrix(matrixStorer);

            this.glSettings.setupGL();
            for (int i = 0; i < this.objects.size(); i++) {
                Object3D o;
                synchronized (objects) {
                    o = this.objects.get(i);
                }
                if (o.isEnabled()) {
                    glSettings.setClientStateNormalArray(o.getType() == Object3D.VBO);
                    glSettings.setClientStateVertexArray(o.getType() == Object3D.VBO);
                    o.draw();
                }
            }

            if (this.sceneSettings.isClearColorAfterRepaint()) {
                GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
            }
            if (this.sceneSettings.isClearDepthAfterRepaint()) {
                GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT);
            }
            for (int index = 0; index < this.thirdDrawScenes.size(); index++) {
                Scene scene;
                synchronized (this.thirdDrawScenes) {
                    scene = this.thirdDrawScenes.get(index);
                }
                scene.draw(this);
            }
        }
    }

    private void sortObjects() {
        this.objectComparator.setCamMatrix(camPos);
        if (this.sceneSettings.isSortTransparentObjects() && this.transparentObjectsOffset < this.objects.size() - 1) {
            this.objectComparator.setSortFunction(this.sceneSettings.getTransparentObjectsSortFunction());
            Collections.sort(objects.subList(this.transparentObjectsOffset + 1, this.objects.size()), this.objectComparator);
        }
        if (this.sceneSettings.isSortNormalObjects() && this.transparentObjectsOffset >= 0) {
            this.objectComparator.setSortFunction(this.sceneSettings.getNormalObjectsSortFunction());
            Collections.sort(objects.subList(0, this.transparentObjectsOffset + 1), this.objectComparator);
        }
    }

    /**
     * Změní všechna nastavení rozměrů, poměrů stran, atd...
     *
     * @param x
     * @param y
     * @param width
     * @param height
     * @param sideRatio
     * @param near
     * @param far
     * @param fov
     * @param centreX
     * @param centreY
     * @param relativeWidth
     * @param invertWidth
     * @param invertHeight
     */
    public void resetup(int x, int y, int width, int height, float sideRatio, float near, float far, float fov, float centreX, float centreY, float relativeWidth, boolean invertWidth, boolean invertHeight) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.sideRatio = sideRatio;
        this.near = near;
        this.far = far;
        this.fov = fov;
        this.centreX = centreX;
        this.centreY = centreY;
        this.relativeWidth = relativeWidth;
        this.invertHeight = invertHeight;
        this.invertWidth = invertWidth;
    }

    /**
     * Změní rozměry scény.
     *
     * @param x
     * @param y
     * @param width
     * @param height
     * @param sideRatio
     */
    public void resize(int x, int y, int width, int height, float sideRatio) {
    }

    /**
     * Základní nastavení GL
     *
     * @param x
     * @param y
     * @param width
     * @param height
     * @param sideRatio
     * @param near
     * @param far
     * @param fov
     */
    private void setupGL(int x, int y, int width, int height, float sideRatio, float near, float far, float fov, float centreX, float centreY, boolean invertWidth, boolean invertHeight) {
        GL11.glViewport(x, y, width, height);
        if (fov != 0) {
            this.setCamFrustum(near, far, sideRatio, fov, centreX, centreY, invertWidth, invertHeight);
        } else {
            this.setCamOrtho(near, far, this.relativeWidth, this.relativeHeight, centreX, centreY, invertWidth, invertHeight);
        }
        this.glSettings.setInvertedView(invertWidth ^ invertHeight);

        GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, this.matrixStorer);
        this.viewFrustum.load(matrixStorer);
        matrixStorer.rewind();

        GL11.glLoadIdentity();
    }

    /**
     * Nastavení pohledové matice na režim pohledové pyramidy
     *
     * @param near
     * @param far
     * @param sideRatio
     * @param fov
     * @param centreX
     * @param centreY
     */
    private void setCamFrustum(float near, float far, float sideRatio, float fov, float centreX, float centreY, boolean invertWidth, boolean invertHeight) {
        float side = (float) Math.tan(Math.toRadians(fov / 2)) * 0.5f;
        float cX = (centreX * 2 * side - side) * -1;
        float cY = (centreY * 2 * (side / sideRatio) - (side / sideRatio)) * -1;
        this.relativeWidth = side * 2;
        this.relativeHeight = (side / sideRatio) * 2;

        float left = -side + cX;
        float right = side + cX;
        float bottom = (-side / sideRatio) + cY;
        float top = (side / sideRatio) + cY;
        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glLoadIdentity();
        GL11.glFrustum(invertWidth ? right : left, invertWidth ? left : right, invertHeight ? top : bottom, invertHeight ? bottom : top, near, far);
        GL11.glMatrixMode(GL11.GL_MODELVIEW);
    }

    /**
     * Nastavení pohledové matice na pravoúhlé promítání.
     *
     * @param near
     * @param far
     * @param width
     * @param height
     * @param centreX
     * @param centreY
     */
    private void setCamOrtho(float near, float far, float width, float height, float centreX, float centreY, boolean invertWidth, boolean invertHeight) {
        this.relativeWidth = width;
        this.relativeHeight = height;

        float left = -(width * centreX);
        float right = width - (width * centreX);
        float bottom = -(height * centreY);
        float top = height - (height * centreY);

        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glLoadIdentity();
        GL11.glOrtho(invertWidth ? right : left, invertWidth ? left : right, invertHeight ? top : bottom, invertHeight ? bottom : top, near, far);
        GL11.glMatrixMode(GL11.GL_MODELVIEW);

    }
/////////////////////////objects///////////////////////////////////////

    /**
     * Přidá objekt do scény
     *
     * @param object
     * @see Object3D
     */
    public void addObject(Object3D object, boolean transparent) {
        if (transparent) {
            synchronized (objects) {
                this.objects.add(object);
            }
        } else {
            synchronized (objects) {
                if (this.transparentObjectsOffset >= 0) {
                    this.objects.add(this.transparentObjectsOffset, object);
                } else {
                    this.objects.add(object);
                }
            }
            this.transparentObjectsOffset++;
        }
    }

    public void removeObject(Object3D object) {
        int index = 0;
        synchronized (objects) {
            index = this.objects.indexOf(object);
        }

        if (index > 0) {
            this.removeObject(index);
        }
    }

    public void removeObject(int index) {
        if (index <= this.transparentObjectsOffset && index >= 0) {
            this.transparentObjectsOffset--;
        }
        synchronized (objects) {
            this.objects.remove(index);
        }
    }

    public Object3D getObject(int index) {
        synchronized (objects) {
            return this.objects.get(index);
        }
    }

    public Object3D[] getObjects() {
        synchronized (objects) {
            return this.objects.toArray(new Object3D[]{});
        }
    }

    public Object3D[] getTransparentObjects() {
        synchronized (objects) {
            return this.objects.subList(this.transparentObjectsOffset + 1, this.objects.size()).toArray(new Object3D[]{});
        }
    }

    public Object3D[] getNonTransparentObjects() {
        synchronized (objects) {
            return this.objects.subList(0, this.transparentObjectsOffset + 1).toArray(new Object3D[]{});
        }
    }

    public void clearObjects() {
        synchronized (objects) {
            this.objects.clear();
        }
        this.transparentObjectsOffset = -1;
    }

    /**
     * Vrátí index posledního neprůhledného objektu.
     *
     * @return
     */
    public int getTransparentObjectsOffset() {
        return transparentObjectsOffset;
    }

///////////////////////////first draw scenes////////////////////////////////
    public void addFirstDrawScene(Scene scene, int index) {
        synchronized (this.firstDrawScenes) {
            if (index >= 0 && index < this.firstDrawScenes.size()) {
                this.firstDrawScenes.add(index, scene);
            } else {
                this.firstDrawScenes.add(scene);
            }
        }
    }

    public void removeFirstDrawScene(Scene scene) {
        synchronized (this.firstDrawScenes) {
            this.firstDrawScenes.remove(scene);
        }
    }

    public void removeFirstDrawScene(int index) {
        synchronized (this.firstDrawScenes) {
            this.firstDrawScenes.remove(index);
        }
    }

    public Scene getFirstDrawScene(int index) {
        synchronized (this.firstDrawScenes) {
            return this.firstDrawScenes.get(index);
        }
    }

    public void moveFirstDrawSceneToPosition(Scene s, int position) {
        this.removeFirstDrawScene(s);
        this.addFirstDrawScene(s, position);
    }
    //////////////////////////second draw scenes//////////////////////////////

    public void addThirdDrawScene(Scene scene, int index) {
        synchronized (this.thirdDrawScenes) {
            if (index >= 0 && index < this.thirdDrawScenes.size()) {
                this.thirdDrawScenes.add(index, scene);
            } else {
                this.thirdDrawScenes.add(scene);
            }
        }
    }

    public void removeThirdDrawScene(Scene scene) {
        synchronized (this.thirdDrawScenes) {
            this.thirdDrawScenes.remove(scene);
        }
    }

    public void removeThirdDrawScene(int index) {
        synchronized (this.thirdDrawScenes) {
            this.thirdDrawScenes.remove(index);
        }
    }

    public Scene getThirdDrawSceneScene(int index) {
        synchronized (this.thirdDrawScenes) {
            return this.thirdDrawScenes.get(index);
        }
    }

    public void moveThirdDrawSceneToPosition(Scene s, int position) {
        this.removeThirdDrawScene(s);
        this.addThirdDrawScene(s, position);
    }
////////////////////////////////////ostatni//////////////////////////////////

    /**
     * Vrací true, pokud je zapnuté uživatelské kreslení.
     *
     * @return
     */
    public boolean isUserDraw() {
        return userDraw;
    }

    /**
     * Zapne/vypne uživatelské kreslení.
     *
     * @param userDraw
     */
    public void setUserDraw(boolean userDraw) {
        this.userDraw = userDraw;
    }

    public float getSideRatio() {
        return sideRatio;
    }

    public Matrix4f getCamPos() {
        return camPos;
    }

    public float getCentreX() {
        return centreX;
    }

    public float getCentreY() {
        return centreY;
    }

    public float getFar() {
        return far;
    }

    public float getFov() {
        return fov;
    }

    public int getHeight() {
        return height;
    }

    public float getNear() {
        return near;
    }

    public int getWidth() {
        return width;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public float getRelativeHeight() {
        return relativeHeight;
    }

    public float getRelativeWidth() {
        return relativeWidth;
    }

    public float getUnitRelativeHeight() {
        return 1 / this.sideRatio;
    }

    public float getUnitRelativeWidth() {
        return 1;
    }

    public SceneSettings getSceneSettings() {
        return sceneSettings;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public GLSettings getGlSettings() {
        return glSettings;
    }
}
